﻿using System;
using Geotools.Geometries;
using System.Collections.Generic;
using AzukiMap.Tools.Logger;
using AzukiMap.Tools;

namespace AzukiMap.Map
{
    public class MapCoordinate
    {
        #region fields

        GTPolygon _currentExtent;
        GTPolygon _lastExtent;
        GTPolygon _renderedExtent;
        GTPoint _geoCenter;

        double _screenWidth;
        double _screenHeight;

        double _screenScale;
        double _geoScale;

        int _srid;
        CoordinationUnits _coordUnit;

        double _angle;
        int _flipY;
        double _adjX;
        double _adjY;

        GeometryUtil _geomUtil;

        #endregion

        #region properties
        public GTPolygon CurrentExtent
        {
            get { return _currentExtent; }
        }

        public GTPolygon LastExtent
        {
            get { return _lastExtent; }
        }

        public GTPolygon RenderedExtent
        {
            get { return _renderedExtent; }
        }

        public double ScreenScale
        {
            get { return _screenScale; }
            set {
                _screenScale = value;
                SetParamsByCenter();
            }
        }

        public double GeoScale
        {
            get { return _geoScale; }
        }

        public GTPoint GeoCenter
        {
            get { return _geoCenter; }
        }

        public double ScreenWidth
        {
            get { return _screenWidth; }
        }

        public double ScreenHeight
        {
            get { return _screenHeight; }
        }

        public int Srid
        {
            set {
                if (value > 0)
                {
                    _srid = value;
                }
                else
                {
                    _srid = -1;
                }
            }
            get { return _srid; }
        }

        public CoordinationUnits MapUnit
        {
            get { return _coordUnit; }
        }

        public double Angle
        {
            set
            {
                _angle = value;
                SetParamsByCenter();
            }
            get { return _angle; }
        }

        public double AdjX
        {
            get { return _adjX; }
            set { _adjX = value; }
        }

        public double AdjY
        {
            get { return _adjY; }
            set { _adjY = value; }
        }

        public bool FlipY
        {
            get { return ( _flipY == 1); }
            set { _flipY = (value) ? 1: -1; }
        }

        public double Tolerance
        {
            get { 
                return _screenScale / 2; 
            }
        }
        #endregion

        # region constructor

        public MapCoordinate()
        {
            _geomUtil = new GeometryUtil();
            initParams(-1);
        }

        public MapCoordinate(int srid)
        {
            _geomUtil = new GeometryUtil(new PrecisionModel(), srid);
            initParams(srid);
        }

        void initParams(int srid)
        {
            _currentExtent = _geomUtil.CreateExtentPolygon(0, 0, 0, 0, 0, 0, 0, 0);
            //_lastExtent = _geomUtil.CreateExtentPolygon(0, 0, 0, 0, 0, 0, 0, 0);
            //_renderedExtent = _geomUtil.CreateExtentPolygon(0, 0, 0, 0, 0, 0, 0, 0);
            _lastExtent = null;
            _renderedExtent = null;
            _flipY = 1;
            _screenWidth = 1.0;
            _screenHeight = 1.0;
            _screenScale = 1.0;
            _angle = 0.0;
            _srid = srid;
            // TODO: get map unit from SRID.
            _coordUnit = CoordinationUnits.DEGREES;
            _geoCenter = _geomUtil.CreatePoint(0, 0);
            SetParamsByCenter();
        }
        # endregion

        #region public methods

        public GTPoint GetGeoPointFromPixel(double x, double y)
        {
            GTCoordinate topLeft = _geomUtil.GetExtentPoint(_currentExtent, ExtentOrder.TOP_LEFT);

            double pixDist = GetPixDistanceFromScreenOrigin(x, y);
            double geoDist = pixDist * _screenScale;
            double angle2Tl = GetAngleFromScreenOrigin(x, y);

            double totalAngle = angle2Tl - _angle;
            double lon = geoDist * Math.Cos(totalAngle) + topLeft.X;
            double lat = geoDist * Math.Sin(totalAngle) * _flipY + topLeft.Y;

            GTPoint geoPoint = _geomUtil.CreatePoint(lon, lat);

            return geoPoint;
        }

        public GTPoint GetPixPointFromGeoOnScreen(double lon, double lat)
        {
            return GetPixPointFromGeo(lon, lat, 0, 0);
        }

        public GTPoint GetPixPointFromGeo(double lon, double lat)
        {
            return GetPixPointFromGeo(lon, lat, _adjX, _adjY);
        }

        public void MoveByPixel(double moveX, double moveY)
        {
            //double centerLon = _geoCenter.X + moveX * _screenScale;
            //double centerLat = _geoCenter.Y + moveY * _screenScale;
            double centerLon = _geoCenter.X + moveX * _screenScale;
            double centerLat = _geoCenter.Y + moveY * _screenScale * _flipY * -1;

            Logger.Debug(
                "MapCoordinate.MoveByPixel: " 
                + " moveX = " + moveX.ToString() + " " 
                + " moveY = " + moveY.ToString() + " "
                + " centerLon = " + centerLon + " "
                + " centerLat = " + centerLat + " "
                + " _screenScale = " + _screenScale
                + " _adjX = " + _adjX
                + " _adjY = " + _adjY
                );

            MoveTo(centerLon, centerLat);
        }

        public void MoveTo(double centerLon, double centerLat)
        {
            double dLength = GetDiagonalLength();
            double dAngle = GetDiagonalAngle();

            double tlAngle = Math.PI - dAngle - _angle;
            double blAngle = Math.PI + dAngle - _angle;
            double brAngle = 2 * Math.PI - dAngle - _angle;
            double trAngle = dAngle - _angle;

            double tlLon = centerLon + dLength / 2 * Math.Cos(tlAngle);
            double blLon = centerLon + dLength / 2 * Math.Cos(blAngle);
            double brLon = centerLon + dLength / 2 * Math.Cos(brAngle);
            double trLon = centerLon + dLength / 2 * Math.Cos(trAngle);

            double tlLat = centerLat + dLength / 2 * Math.Sin(tlAngle) * _flipY;
            double blLat = centerLat + dLength / 2 * Math.Sin(blAngle) * _flipY;
            double brLat = centerLat + dLength / 2 * Math.Sin(brAngle) * _flipY;
            double trLat = centerLat + dLength / 2 * Math.Sin(trAngle) * _flipY;

            _geoCenter = _geomUtil.CreatePoint(centerLon, centerLat);

            double exWidth = _geomUtil.GetPolygonEnvelopeWidth(_currentExtent);
            if (exWidth != 0)
            {
                SetPreviousExtents();
            }
            _currentExtent = _geomUtil.CreateExtentPolygon(tlLon, tlLat, blLon, blLat, brLon, brLat, trLon, trLat);

            Logger.Debug(
                "MapCoordinate.SetCenter(2): "
                + " centerLon = " + centerLon.ToString()
                + " centerLat = " + centerLat.ToString()
                + " _geoCenter = " + _geoCenter.ToText()
                + " dLength = " + dLength.ToString()
                + " dAngle = " + dAngle.ToString()
                + " _currentExtent = " + _currentExtent.ToText()
                );
        }

        public void MoveTo(double centerLon, double centerLat, double screenScale)
        {
            _screenScale = screenScale;
            SetGeoScaleByScreenScale(_screenScale);

            Logger.Debug(
                "MapCoordinate.SetCenter(3): "
                + " centerLon = " + centerLon.ToString()
                + " centerLat = " + centerLat.ToString()
                + " screenScale = " + screenScale.ToString()
                );

            MoveTo(centerLon, centerLat);
            ClearPreviousExtents();
        }

        public void MoveTo(double centerLon, double centerLat, double screenScale, double angle)
        {
            _angle = angle;

            Logger.Debug(
                "MapCoordinate.SetCenter(4): "
                + " centerLon = " + centerLon.ToString()
                + " centerLat = " + centerLat.ToString()
                + " screenScale = " + screenScale.ToString()
                + " angle = " + angle.ToString()
                );

            MoveTo(centerLon, centerLat, screenScale);
        }

        /// <summary>
        /// Set the map extent by 2 points.
        /// When the map is rotated(angle is not 0), the map extent will be a polygon that contains the 2 points
        /// on the polygon edges.
        /// </summary>
        /// <param name="minX"></param>
        /// <param name="minY"></param>
        /// <param name="maxX"></param>
        /// <param name="maxY"></param>
        /// <returns></returns>
        public GTPolygon MoveToExtent(double minLon, double minLat, double maxLon, double maxLat)
        {
            double verticalAngle = _angle * -1;
            double horizontalAngle = Math.PI / 2 - _angle;

            // get Y-intersects of lines through the extent vertexes.
            double[] vYIntersects = GetYIntersectsFromExtent(minLon, minLat, maxLon, maxLat, verticalAngle);
            double vLowYIntersect = vYIntersects[0];
            double vHighYIntersect = vYIntersects[1];

            double[] hYIntersects = GetYIntersectsFromExtent(minLon, minLat, maxLon, maxLat, horizontalAngle);
            double hLowYIntersect = hYIntersects[0];
            double hHighYIntersect = hYIntersects[1];
            
            // get minimum extent with angle that can contain requested area.
            GTPoint tl = GetIntersectionByAngles(horizontalAngle, verticalAngle, hHighYIntersect, vHighYIntersect);
            GTPoint bl = GetIntersectionByAngles(horizontalAngle, verticalAngle, hHighYIntersect, vLowYIntersect);
            GTPoint br = GetIntersectionByAngles(horizontalAngle, verticalAngle, hLowYIntersect, vLowYIntersect);
            GTPoint tr = GetIntersectionByAngles(horizontalAngle, verticalAngle, hLowYIntersect, vHighYIntersect);

            // minimum extent width and screen scales.
            double minWidth = Math.Sqrt(Math.Pow(tl.X - tr.X, 2) + Math.Pow(tl.Y - tr.Y, 2));
            double minHeight = Math.Sqrt(Math.Pow(tl.X - bl.X, 2) + Math.Pow(tl.Y - bl.Y, 2));

            SetScreenScale(minWidth, minHeight);

            double centerLon = (minLon + maxLon) / 2;
            double centerLat = (minLat + maxLat) / 2;

            Logger.Debug(
                "MapCoordinate.SetExtent(): "
                + " minLon = " + minLon.ToString()
                + " minLat = " + minLat.ToString()
                + " maxLon = " + maxLon.ToString()
                + " maxLat = " + maxLat.ToString()
                + " centerLon = " + centerLon.ToString()
                + " centerLat = " + centerLat.ToString()
                );
            MoveTo(centerLon, centerLat);
            SetParamsByCenter();
            return _currentExtent;
        }

        public void SetScreenSize(double width, double height)
        {
            double geoWidth = _geomUtil.GetExtentPolygonWidth(_currentExtent);
            double geoHeight = _geomUtil.GetExtentPolygonHeight(_currentExtent);
            //double scaleX = (geoHeight * Math.Sin(_angle) + geoWidth * Math.Cos(_angle)) / _screenWidth;
            //double scaleY = (geoHeight * Math.Cos(_angle) + geoWidth * Math.Sin(_angle)) / _screenHeight;
            double scaleX = geoWidth / width;
            double scaleY = geoHeight / height;

            _screenScale = (scaleX >= scaleY) ? scaleX : scaleY;
            _screenWidth = width;
            _screenHeight = height;
            SetGeoScaleByScreenScale(_screenScale);

            Logger.Debug(
                "MapCoordinate.SetScreenSize: "
                + " width = " + width.ToString()
                + " height = " + height.ToString()
                + " geoWidth = " + geoWidth.ToString()
                + " geoHeight = " + geoHeight.ToString()
                + " scaleX = " + scaleX.ToString()
                + " scaleY = " + scaleY.ToString()
                + " _screenScale = " + _screenScale.ToString()
                );

            SetParamsByCenter();
            ClearPreviousExtents();
        }

        public double ZoomByRatio(double zoomRatio)
        {
            if (zoomRatio <= 0)
            {
                throw new ArgumentOutOfRangeException("zoomRatio should be > 0");
            }
            _screenScale *= zoomRatio;
            SetGeoScaleByScreenScale(_screenScale);

            Logger.Debug(
                "MapCoordinate.SetScreenSize: "
                + " zoomRatio = " + zoomRatio.ToString()
                + " _screenScale = " + _screenScale.ToString()
                );

            SetParamsByCenter();
            ClearPreviousExtents();
            return _screenScale;
        }

        public void ZoomByScale(double geoScale)
        {
            if (geoScale <= 0)
            {
                throw new ArgumentOutOfRangeException("geoScale should be > 0");
            }
            _geoScale = geoScale;
            SetScreenScaleByGeoScale(geoScale);
            SetParamsByCenter();
            ClearPreviousExtents();
        }

        public double ZoomByRatioAndAnchor(double zoomRatio, double anchorX, double anchorY)
        {
            if (zoomRatio <= 0)
            {
                throw new ArgumentOutOfRangeException("zoomRatio should be > 0");
            }
            anchorX += _adjX;
            anchorY += _adjY;

            GTPoint anchorPoint = GetGeoPointFromPixel(anchorX, anchorY);
            GTCoordinate currentOrigin = _geomUtil.GetExtentPoint(_currentExtent, ExtentOrder.TOP_LEFT);

            double newScale = _screenScale * zoomRatio;

            double lx = anchorPoint.X - currentOrigin.X;
            double ly = anchorPoint.Y - currentOrigin.Y * _flipY;

            double originX = anchorPoint.X - lx * zoomRatio;
            double originY = anchorPoint.Y - ly * zoomRatio;
            double width = _screenWidth * newScale;
            double height = _screenHeight * newScale;

            double newCenterX = originX + width / 2;
            double newCenterY = originY - height / 2 * _flipY;

            _screenScale = newScale;
            SetGeoScaleByScreenScale(newScale);

            Logger.Debug(
                "MapCoordinate.ZoomByRatioAndPixel: "
                + " anchorX = " + anchorX.ToString()
                + " anchorY= " + anchorY.ToString()
                + " zoomRatio = " + zoomRatio.ToString()
                + " newScale = " + newScale.ToString()
                + " width = " + width.ToString()
                + " height = " + height.ToString()
                + " newCenterX = " + newCenterX.ToString()
                + " newCenterY = " + newCenterY.ToString()
                + " lx = " + lx.ToString()
                + " ly = " + ly.ToString()
                + " originX = " + originX.ToString()
                + " originY = " + originY.ToString()
                );

            MoveTo(newCenterX, newCenterY);
            ClearPreviousExtents();

            return _screenScale;
        }

        public GTCoordinate GetExtentPoint(ExtentOrder position)
        {
            return _geomUtil.GetExtentPoint(_currentExtent, position);
        }

        #endregion

        #region private methods

        GTPoint GetPixPointFromGeo(double lon, double lat, double adjX, double adjY)
        {
            double distFromCenter = GetDistanceFromCenter(lon, lat);
            double angle2Center = GetAngleToCenter(lon, lat);

            double totalAngle = angle2Center - _angle;

            double x = (distFromCenter / _screenScale) * Math.Cos(totalAngle) + _screenWidth / 2 - adjX;
            double y = (distFromCenter / _screenScale) * Math.Sin(totalAngle) + _screenHeight / 2 - adjY;

            GTPoint pixPoint = _geomUtil.CreatePoint(x, y);
            return pixPoint;
        }

        double GetAngleToCenter(double lon, double lat)
        {
            double distFromCenter = GetDistanceFromCenter(lon, lat);
            double angle2Center = 0;
            if (distFromCenter != 0)
            {
                angle2Center = Math.Asin((_geoCenter.Y - lat) * _flipY / distFromCenter);
                if (lon < _geoCenter.X)
                {
                    angle2Center = Math.PI - angle2Center;
                }
            }

            return angle2Center;
        }

        double GetDistanceFromCenter(double lon, double lat)
        {
            return Math.Sqrt(Math.Pow((_geoCenter.X - lon),2) + Math.Pow((_geoCenter.Y - lat),2));
        }

        double GetAngleFromScreenOrigin(double x, double y)
        {
            double a = 0;
            double r = GetPixDistanceFromScreenOrigin(x, y);
            if (r != 0)
            {
                a = Math.Asin(y * -1 / r);
                if (x < 0)
                {
                    a = Math.PI - a;
                }
            }
            return a;
        }

        double GetPixDistanceFromScreenOrigin(double x, double y)
        {
            return Math.Sqrt( Math.Pow(x, 2) + Math.Pow(y,2));
        }

        /*
        internal void MoveByPixel(double moveX, double moveY, double adjX, double adjY)
        {
            _adjX = adjX;
            _adjY = adjY;
            MoveByPixel(moveX, moveY);
        }
         */

        void SetPreviousExtents()
        {
            _lastExtent = _currentExtent;
            if (_renderedExtent == null)
            {
                _renderedExtent = _lastExtent;
            }
            else
            {
                // TODO: fix union method
                //_renderedExtent.Union(_lastExtent);
                _renderedExtent = _lastExtent;
            }
        }

        void ClearPreviousExtents()
        {
            _lastExtent = null;
            _renderedExtent = null;
        }

        void SetParamsByCenter()
        {
            Logger.Debug(
                "MapCoordinate.SetParamsByCenter: "
                );
            MoveTo(_geoCenter.X, _geoCenter.Y);
        }

        public GTPoint GetCenter()
        {
            return _geomUtil.GetPolygonCenter(_currentExtent);
        }

        void SetScreenScale(double geoWidth, double geoHeight)
        {
            double scaleX = geoWidth / _screenWidth;
            double scaleY = geoHeight / _screenHeight;
            _screenScale = (scaleX >= scaleY) ? scaleX : scaleY;
            SetGeoScaleByScreenScale(_screenScale);
        }

        void SetGeoScaleByScreenScale(double screenScale)
        {
            _geoScale = screenScale * AzukiMapSettings.DPI * AzukiMapSettings.InchesPerUnit(MapUnit);
        }

        void SetScreenScaleByGeoScale(double geoScale)
        {
            _screenScale = geoScale / (AzukiMapSettings.DPI * AzukiMapSettings.InchesPerUnit(MapUnit)); 
        }


        double[] GetYIntersectsFromExtent(double minx, double miny, double maxx, double maxy, double angle)
        {
            List<double> yIntersects = new List<double>(4);
            yIntersects.Add(-Math.Tan(angle) * maxx + maxy);
            yIntersects.Add(-Math.Tan(angle) * maxx + miny);
            yIntersects.Add(-Math.Tan(angle) * minx + maxy);
            yIntersects.Add(-Math.Tan(angle) * minx + miny);

            yIntersects.Sort();

            double[] yIntersectsMinMax = { yIntersects[0], yIntersects[3]};

            return yIntersectsMinMax;
        }

        GTPoint GetIntersectionByAngles(double hAngle, double vAngle, double hYIntersect, double vYIntersect)
        {
            double x = (vYIntersect - hYIntersect) / (Math.Tan(hAngle) - Math.Tan(vAngle));
            double y = Math.Tan(vAngle) * x + vYIntersect;
            GTPoint intersection = _geomUtil.CreatePoint(x, y);
            return intersection;
        }

        GTPolygon SetExtent(GTPolygon extent)
        {
            throw new NotImplementedException();
        }

        double GetDiagonalLength()
        {
            double geoWidth = _screenWidth * _screenScale;
            double geoHeight = _screenHeight * _screenScale;
            double diagonal = Math.Sqrt(Math.Pow(geoWidth, 2) + Math.Pow(geoHeight, 2));
            return diagonal;
        }

        double GetDiagonalAngle()
        {
            double dAngle = Math.Atan(_screenHeight / _screenWidth);
            return dAngle;
        }

        #endregion
    }
}
